Skip to content

Remove vals from BindingsArray#11642

Merged
chriseclectic merged 12 commits into
Qiskit:mainfrom
ihincks:primitives/no-vals-bindings-array
Jan 31, 2024
Merged

Remove vals from BindingsArray#11642
chriseclectic merged 12 commits into
Qiskit:mainfrom
ihincks:primitives/no-vals-bindings-array

Conversation

@ihincks
Copy link
Copy Markdown
Contributor

@ihincks ihincks commented Jan 25, 2024

Summary

In the spirit of having only one way to do things, this PR reduces the complexity of the BindingsArray class to only support the kwvals case, i.e. the case where parameter values are attached to their names. The other motivation for opening this PR is that we were running into confusing technical difficulties when trying to reason about both vals and kwvals at the same time.

Details and comments

Note that this change would not affect primitive user's ability to submit parameter values as a big numpy array, as seen in some of the examples. This is because SamplerPub and EstimatorPub know what the parameter names should be, and can therefore inject them if needed. This is also implemented in this PR.

@ihincks ihincks requested review from a team as code owners January 25, 2024 16:42
@qiskit-bot
Copy link
Copy Markdown
Collaborator

One or more of the the following people are requested to review this:

  • @Qiskit/terra-core
  • @ajavadia
  • @ikkoham
  • @levbishop
  • @t-imamichi

Copy link
Copy Markdown
Member

@chriseclectic chriseclectic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a nice simplification. Couple of non-blocking questions

Comment thread qiskit/primitives/containers/bindings_array.py
Comment thread qiskit/primitives/containers/bindings_array.py Outdated
Comment thread qiskit/primitives/containers/bindings_array.py Outdated
@chriseclectic
Copy link
Copy Markdown
Member

I forgot to mention before, but can we add the as_array method back in this PR now that this refactor avoids the previous issues with validating the parameters.

self,
vals: ArrayLike | Iterable[ArrayLike] | None = None,
kwvals: Mapping[ParameterLike, Iterable[ParameterValueType]] | ArrayLike | None = None,
data: Mapping[ParameterLike, Iterable[ParameterValueType]] | ArrayLike | None = None,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ArrayLike allowed as data? The following case raises an error.

In [5]: ba = BindingsArray([])
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[5], line 1
----> 1 ba = BindingsArray([])

File ~/tasks/4_2024/qiskit/terra/qiskit/primitives/containers/bindings_array.py:98, in BindingsArray.__init__(self, data, shape)
     92     self._data = {}
     93 else:
     94     self._data = {
     95         _format_key((p,))
     96         if isinstance(p, Parameter)
     97         else _format_key(p): np.array(val, copy=False)
---> 98         for p, val in data.items()
     99     }
    101 self._shape = _infer_shape(self._data) if shape is None else shape_tuple(shape)
    102 self._num_parameters = None

AttributeError: 'list' object has no attribute 'items'

Copy link
Copy Markdown
Member

@chriseclectic chriseclectic Jan 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@t-imamichi No, with this change its not. A zero param binding array would need to be initialized as BindingsArray(), BindingsArray(None) or BindingsArray({}). So i think the type here is wrong, the | ArrayLike should be removed

@t-imamichi
Copy link
Copy Markdown
Member

I made a patch of test_statevector_samplers to fix errors in CI. ihincks#1

@t-imamichi
Copy link
Copy Markdown
Member

t-imamichi commented Jan 26, 2024

It makes sense to simplify the data structure and I agree with the idea.
JFYI, but I noticed that vals was convenient when parameters are automatically generated such as RealAmplitudes.
This PR asks us to generate a list of parameters explicitly as follows.

vals = np.array(...)

# this PR
qc = RealAmplitudes(2, reps=2)
params = (param.name for param in qc.parameters)
sampler.run([(qc, {params: vals}], shots=100)

# currently
qc = RealAmplitudes(2, reps=2)
sampler.run([(qc, vals)], shots=100)

vals also allows multiple results with non-parameteric circuit as follows.

qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
result = sampler.run([(qc, [(), (), ()]).result()
print(result[0].data)
# DataBin<3>(c=BitArray(<shape=(3,), num_shots=1024, num_bits=2>))

@chriseclectic
Copy link
Copy Markdown
Member

@t-imamichi I believe the coerce method of SamplerPub (and hence sampler.run) can still infer the parameters from a circuit and ndarray in a pub-like format. This change is just effects initializing a bindings array directly. Though in this case you can do {tuple(circuit.parameters): array} (aside: maybe circuit.parameters type should be hashable so you can avoid the explicit tuple cast).

More than simplification, the reason for this is there were numerous ambiguities when mixing vals and kwvals in bindings array since some parameters are named, some arent, and some are assumed to have a fixed order, while others arent, which leads to issues when a pub is finally executed and values need to be assigned to a circuit. This meant there were basically two viable options: allow only vals or kwvals in a BindingsArray, but not both, or remove one of them. Removing one of them simplifies the internals quite a lot so I think the small inconvenience in this construction is probably worth it.

@ihincks ihincks force-pushed the primitives/no-vals-bindings-array branch from bfd7949 to 1990a8d Compare January 30, 2024 21:26
@coveralls
Copy link
Copy Markdown

coveralls commented Jan 30, 2024

Pull Request Test Coverage Report for Build 7728484593

  • -5 of 67 (92.54%) changed or added relevant lines in 3 files are covered.
  • 8 unchanged lines in 2 files lost coverage.
  • Overall coverage increased (+0.02%) to 89.597%

Changes Missing Coverage Covered Lines Changed/Added Lines %
qiskit/primitives/containers/bindings_array.py 48 53 90.57%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/lex.rs 2 91.69%
crates/qasm2/src/parse.rs 6 97.62%
Totals Coverage Status
Change from base Build 7728099338: 0.02%
Covered Lines: 59334
Relevant Lines: 66223

💛 - Coveralls

Comment on lines -138 to -141
[[np.pi]],
np.array([np.pi]),
np.array([[np.pi]]),
[np.array([np.pi])],
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change in behaviour because the outer list of vals enabled (1,1) things to be honourary floats. I don't see any reason to keep that because the only reason (1,) shaped things are honourary floats is to have {("a",): [0]}. behave like {("a","b"): [0,1]}.

t-imamichi
t-imamichi previously approved these changes Jan 31, 2024
Copy link
Copy Markdown
Member

@t-imamichi t-imamichi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good.

Minor comment. I see the pattern BindingsArray({tuple(circuit.parameters): values}) often appears in the codes. It might be useful to introduce a utlity method such as BindingsArray.from_pair(circuit: QuantumCircuit, values: ArrayLike) -> BindingsArray. But it's just a one-line code and I understand Chris' comment #11642 (comment), so it's not a strong opinion.

@ihincks
Copy link
Copy Markdown
Contributor Author

ihincks commented Jan 31, 2024

Happy to add BindingsArray.from_pair(circuit: QuantumCircuit, values: ArrayLike) -> BindingsArray for 1.1. Thanks for the review!

Copy link
Copy Markdown
Member

@chriseclectic chriseclectic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main remaining issue is the 1-parameter array edges cases:

for i, shape in enumerate([(), (1,), (3,), (1, 1), (3, 1)]):
    try:
        ba = BindingsArray({"a": np.zeros(shape)})
        print(i, "Input array shape:", shape, ",\tas_array() array shape:", ba.as_array().shape)
    except Exception as ex:
        print(i, ex)

raises exceptions for the last two cases:

0 Input array shape: () ,	as_array() array shape: (1,)
1 Input array shape: (1,) ,	as_array() array shape: (1, 1)
2 Input array shape: (3,) ,	as_array() array shape: (3, 1)
3 Could not unambiguously determine the intended shape, all shapes in {(1,), (1, 1)} are consistent with the input; specify shape manually.
4 Could not unambiguously determine the intended shape, all shapes in {(3, 1), (3,)} are consistent with the input; specify shape manually.

For a ndim > 1 input array with the last axis 1, we should probably default to treating it as the parameter axis unless a user specifies otherwise, since that is the shape that would be returned by as_array.

Im happy to merge this as is though and think about this some more for a followup PR

@ihincks
Copy link
Copy Markdown
Contributor Author

ihincks commented Jan 31, 2024

@chriseclectic I think b465b97 covers the cases you discussed.

@chriseclectic chriseclectic added this pull request to the merge queue Jan 31, 2024
Merged via the queue into Qiskit:main with commit 43ea026 Jan 31, 2024
@ihincks ihincks deleted the primitives/no-vals-bindings-array branch February 1, 2024 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod: primitives Related to the Primitives module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants